-- Viral Resistance - T-Engine 4 Module
-- Copyright (C) 2010 Mikolai Fajer
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Mikolai Fajer "yufra"
-- mfajer@gmail.com

require "engine.class"
require "engine.Actor"
require "engine.Autolevel"
require "engine.interface.ActorInventory"
require "engine.interface.ActorTemporaryEffects"
require "engine.interface.ActorProject"
require "engine.interface.ActorLevel"
require "engine.interface.ActorStats"
require "engine.interface.ActorTalents"
require "engine.interface.ActorQuest"
require "engine.interface.ActorResource"
require "engine.interface.ActorFOV"
require "mod.class.interface.Combat"
require "mod.class.interface.ActorLife"
require "engine.Faction"
require "engine.Map"
require "mod.class.Object"

module(..., package.seeall, class.inherit(
	engine.Actor,
	engine.interface.ActorInventory,
	engine.interface.ActorTemporaryEffects,
	mod.class.interface.ActorLife,
	engine.interface.ActorProject,
	engine.interface.ActorLevel,
	engine.interface.ActorStats,
	engine.interface.ActorTalents,
	engine.interface.ActorQuest,
	engine.interface.ActorResource,
	engine.interface.ActorFOV,
	mod.class.interface.Combat
))

_M.__do_distance_map = true

function _M:init(t, no_default)
	-- Define some basic combat stats
	t.combat_armor = 0

	-- Default regen
	t.vload_regen = t.vload_regen or 1
	t.life_regen = t.life_regen or 0.25 -- Life regen real slow
	t.stamina_regen = t.stamina_regen or 0.3
	t.vload = t.vload or 0

	-- Default melee barehanded damage
	t.combat = t.combat or { dam=1, }
	
	t.movement_speed = t.movement_speed or 1
	t.climbing_speed = t.climbing_speed or 0.5
	
	t.unused_stats = t.unused_stats or 0
	t.unused_talents =  t.unused_talents or 0
    t.unused_generics = t.unused_generics or 0
	t.unused_talents_types = t.unused_talents_types or 0
	
    t.talent_cd_reduction = t.talent_cd_reduction or {}

	t.lite = t.lite or 0

	engine.Actor.init(self, t, no_default)
	engine.interface.ActorInventory.init(self, t)
	engine.interface.ActorTemporaryEffects.init(self, t)
	mod.class.interface.ActorLife.init(self, t)
	engine.interface.ActorProject.init(self, t)
	engine.interface.ActorTalents.init(self, t)
	engine.interface.ActorResource.init(self, t)
	engine.interface.ActorStats.init(self, t)
	engine.interface.ActorLevel.init(self, t)
	engine.interface.ActorFOV.init(self, t)
end

function _M:act()
	if not engine.Actor.act(self) then return end

	self.changed = true

	-- Cooldown talents
	self:cooldownTalents()
	-- Regen resources
	self:regenLife()
	self:regenVload()
	self:regenResources()
	-- Compute timed effects
	self:timedEffects()

	if self:attr("stunned") then self.energy.value = 0 end

	-- Still enough energy to act ?
	if self.energy.value < game.energy_to_act then return false end

	return true
end

-- Uses the engine.Map:checkAllEntitiesNoStop, so block_move will be called on ALL entities
function _M:move(x, y, force)
	if force then engine.Actor.move(self, x, y, force); return true end
	-- Check for talking, climbing, block_move, etc.
	local ox, oy = self.x, self.y
	local moved = false
	for entity, ret in pairs(game.level.map:checkAllEntitiesNoStop(x, y, "block_move", self, true)) do
		moved = moved or ret
	end
	local energy_per_step
	if self:attr("never_move") then
		moved = true
	elseif not moved then
		-- Check if the actor is climbing or running
		local stamina_per_step
		if self:hasEffect(self.EFF_CLIMBING) then
			stamina_per_step = 1 + 10 * self:getEncumbrance() / self:getMaxEncumbrance()
			game.logSeen(self, "%s climbs onto a %s.", self.name, game.level.map(x, y, engine.Map.TERRAIN).name)
		elseif self:isTalentActive(self.T_RUNNING) then
			stamina_per_step = 1 + 10 * self:getEncumbrance() / self:getMaxEncumbrance()
		end
		-- Compute the energy/speed for this movement
		energy_per_step = game.energy_to_act / math.max(0.1, self:attr("movement_speed"))
		if not stamina_per_step or self:getStamina() >= stamina_per_step then
			moved = engine.Actor.move(self, x, y, true)
			if stamina_per_step and self.x == x and self.y ==y then self:incStamina(-stamina_per_step) end
		else
			moved = true
			game.logPlayer(self, "#FF0000#You are too tired to move!")
		end
	end
	if moved and not self.did_energy then self:useEnergy(energy_per_step) end
	self.did_energy = false
	if moved and not force and ox and oy and (ox ~= self.x or oy ~= self.y) then
		self:setMoveAnim(ox, oy, 3)
	end
	return moved
end

function _M:tooltip()
	local factcolor, factstate = "#ANTIQUE_WHITE#", "neutral"
	if self:reactionToward(game.player) < 0 then factcolor, factstate = "#LIGHT_RED#", "hostile"
	elseif self:reactionToward(game.player) > 0 then factcolor, factstate = "#LIGHT_GREEN#", "friendly"
	end

	local effs = {}
	for tid, act in pairs(self.sustain_talents) do
		if act then effs[#effs+1] = ("- #LIGHT_GREEN#%s"):format(self:getTalentFromId(tid).name) end
	end
	for eff_id, p in pairs(self.tmp) do
		local e = self.tempeffect_def[eff_id]
		if e.status == "detrimental" then
			effs[#effs+1] = ("- #LIGHT_RED#%s"):format(e.desc)
		else
			effs[#effs+1] = ("- #LIGHT_GREEN#%s"):format(e.desc)
		end
	end

	return ([[%s
#00ffff#Level: %d
#ff0000#HP: %d (%d%%)
#00ff00#Viral Load: %d
Stats: %d / %d / %d
%s
Faction: %s%s (%s)
%s]]):format(
	self.name,
	self.level,
	self.life, self.life * 100 / self.max_life,
	self.vload,
	self:getStr(),
	self:getDex(),
	self:getCon(),
	self.desc or "",
	factcolor, engine.Faction.factions[self.faction].name, factstate,
	table.concat(effs, "\n")
	)
end

function _M:onTakeHit(value, src)
	return value
end

function _M:die(src, mutated)
	mod.class.interface.ActorLife.die(self, src)

	-- Gives the killer some exp for the kill
	if src and src.gainExp then
		src:gainExp(self:worthExp(src))
	end

	-- Player killed this actor?
	if src and src.player then
		-- Achievements
		-- Record kills
		src.all_kills = src.all_kills or {}
		src.all_kills[self.name] = src.all_kills[self.name] or 0
		src.all_kills[self.name] = src.all_kills[self.name] + 1
	end
	
	-- Finally drop the body and stuff
	if not mutated and self.corpse then
		local corpse = mod.class.Object.new(self.corpse)
		game.zone:addEntity(game.level, corpse, "object", self.x, self.y)
	end
	for inven_id, inven in pairs(self.inven) do
		for i, o in ipairs(inven) do
			if not o.no_drop then
				o.droppedBy = self.name
				game.level.map:addObject(self.x, self.y, o)
			end
		end
	end
	self.inven = {}
	return true
end

function _M:levelup()
	-- Increase the stats and talents
	self.unused_stats = self.unused_stats + 1
	self.unused_talents = self.unused_talents + 1
	self.unused_generics = self.unused_generics + 1

	-- Heal up on new level
	self.life = self.max_life
	self.stamina = self:getMaxStamina()
	if self.type ~= "zombie" then
		self.vload = 0
	end
end

--- Notifies a change of stat value
function _M:onStatChange(stat, v)
	if stat == self.STAT_CON then
		self.max_life = self.max_life + 2
	end
end

function _M:attack(target)
	self:bumpInto(target)
end

--- Gets the talent cooldown, checking cooldown reduction
-- @param t the talent to get the cooldown for
function _M:getTalentCooldown(t)
    if not t.cooldown then return end
    local cd
    if type(t.cooldown) == "function" then
    else
        cd = t.cooldown
    end
    if self.talent_cd_reduction[t.id] then cd = math.max(0, cd - self.talent_cd_reduction[t.id]) end
    return cd
end

--- Starts a talent cooldown; overloaded from the default to handle talent cooldown reduction
-- @param t the talent to cooldown
function _M:startTalentCooldown(t)
	local cd = self:getTalentCooldown(t)
	self.talents_cd[t.id] = cd
	self.changed = true
end

--- Called before a talent is used
-- Check the actor can cast it
-- @param ab the talent (not the id, the table)
-- @param silent should we suppress and log messages?
-- @param provided_targets table of targets to validate ({x=?, y=?})
-- @return true to continue, false to stop
function _M:preUseTalent(ab, silent, fake, provided_targets)
	if not self:enoughEnergy() and not fake then return false end

	-- Allow for silent talents
	if ab.mode == "sustained" then
		if ab.sustain_stamina and self.getMaxStamina() < ab.sustain_stamina and not self:isTalentActive(ab.id) then
			game.logPlayer(self, "You do not have enough stamina to activate %s.", ab.name)
			return false
		end
	else
		if ab.stamina and self:getStamina() < ab.stamina then
			game.logPlayer(self, "You do not have enough stamina to use %s.", ab.name)
			return false
		end
		if ab.viral_req and self.vload < ab.viral then
			game.logPlayer(self, "You cannot use %s without at least %d viral load.", ab.name, ab.viral_req)
			return false
		end
	end
	-- Check for special preuse
	if ab.on_preuse and not ab.on_preuse(self, ab, silent) then
		return false
	end
	-- Verifies target and range if this is for AI or MapMenu
--	if provided_targets and not self:validateTalentTargets(ab, provided_targets) then return false end
	-- Finally display the message
	if not silent then
		if ab.message ~= nil then
			if ab.message then
				game.logSeen(self, "%s", self:useTalentMessage(ab))
			end
		elseif ab.mode == "sustained" and not self:isTalentActive(ab.id) then
			game.logSeen(self, "%s activates %s.", self.name:capitalize(), ab.name)
		elseif ab.mode == "sustained" and self:isTalentActive(ab.id) then
			game.logSeen(self, "%s deactivates %s.", self.name:capitalize(), ab.name)
		else
			game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name)
		end
	end
	return true
end

--- Called before a talent is used
-- Check if it must use a turn, mana, stamina, ...
-- @param ab the talent (not the id, the table)
-- @param ret the return of the talent action
-- @return true to continue, false to stop
function _M:postUseTalent(ab, ret)
	if not ret then return end
    
    if ab.mode == "sustained" then
		if not self:isTalentActive(ab.id) then
			if ab.sustain_stamina then
                self:incMaxStamina(-ab.sustain_stamina)
			end
        else
            if ab.sustain_stamina then
                self:incMaxStamina(ab.sustain_stamina)
            end
        end
    else
        if ab.stamina then
			self:incStamina(-ab.stamina)
		end
        if ab.viral then
            self:takeVloadHit(ab.viral, ab)
        end
    end
    
	self:useEnergy()

	-- Cancel stealth!
	if ab.id ~= self.T_STEALTH then self:breakStealth() end

	return true
end

--- Breaks stealth if active
function _M:breakStealth()
	if self:isTalentActive(self.T_STEALTH) then
		local old = self.energy.value
		self.energy.value = 100000
		self:useTalent(self.T_STEALTH)
		self.energy.value = old
		self.changed = true
	end
end

--- Return the full description of a talent
-- @param t the talent to describe
-- @param addlevel the offset from the current talent level to describe
function _M:getTalentFullDescription(t, addlevel)
	local old = self.talents[t.id]
	self.talents[t.id] = (self.talents[t.id] or 0) + (addlevel or 0)

	local d = tstring{}

	d:add({"color",0x6f,0xff,0x83}, "Effective talent level: ", {"color",0x00,0xFF,0x00}, ("%.1f"):format(self:getTalentLevel(t)), true)
	if t.mode == "passive" then d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Passive", true)
	elseif t.mode == "sustained" then d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Sustained", true)
	else d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Activated", true)
	end

	if t.stamina or t.sustain_stamina then d:add({"color",0x6f,0xff,0x83}, "Stamina cost: ", {"color",0xFF,0xFF,0xFF}, ""..(t.sustain_stamina or t.stamina), true) end
	if t.viral or t.sustain_viral then d:add({"color",0x6f,0xff,0x83}, "Viral cost: ", {"color",0xFF,0xFF,0xFF}, ""..(t.viral or t.sustain_viral), true) end
	if t.viral_req then d:add({"color",0x6f,0xff,0x83}, "Minimum viral load: ", {"color",0xFF,0xFF,0xFF}, ""..t.viral_req, true) end

	if self:getTalentRange(t) > 1 then d:add({"color",0x6f,0xff,0x83}, "Range: ", {"color",0xFF,0xFF,0xFF}, ""..self:getTalentRange(t), true)
	else d:add({"color",0x6f,0xff,0x83}, "Range: ", {"color",0xFF,0xFF,0xFF}, "melee/personal", true)
	end
	if t.cooldown then d:add({"color",0x6f,0xff,0x83}, "Cooldown: ", {"color",0xFF,0xFF,0xFF}, ""..util.getval(t.cooldown, self, t), true) end
	local speed = self:getTalentProjectileSpeed(t)
	if speed then d:add({"color",0x6f,0xff,0x83}, "Travel Speed: ", {"color",0xFF,0xFF,0xFF}, ""..(speed * 100).."% of base", true)
	else d:add({"color",0x6f,0xff,0x83}, "Travel Speed: ", {"color",0xFF,0xFF,0xFF}, "instantaneous", true)
	end

	d:add({"color",0x6f,0xff,0x83}, "Description: ", {"color",0xFF,0xFF,0xFF}, t.info(self, t), true)

	self.talents[t.id] = old

	return d
end

--- How much experience is this actor worth
-- @param target to whom is the exp rewarded
-- @return the experience rewarded
function _M:worthExp(target)
	if not target.level or self.level < target.level - 3 then return 0 end

	local mult = 2
	if self.unique then mult = 6
	elseif self.egoed then mult = 3 end
	return self.level * mult * self.exp_worth
end

--- Can the actor see the target actor
-- This does not check LOS or such, only the actual ability to see it.<br/>
-- Check for stealth, ...
function _M:canSee(actor, def, def_pct)
	if not actor then return false, 0 end
	local hit = true
	local chance = 100
	
	-- Check for stealth. Checks against the target cunning and level
	if actor:attr("stealth") and actor ~= self then
		local def = self.level / 2 + self:getCun(20, true)
		local current_chance = math.max(5, math.min(95, 100 + def - actor:attr("stealth")))
		local hit = rng.percent(current_chance)
		print(string.format("[STEALTH] %s can see %s: %s %.2f", self.name, actor.name, tostring(hit), current_chance))
		chance = chance * (current_chance / 100)
		if not hit then
			return false, chance
		end
	end

	return hit, chance
end

--- Can the target be applied some effects
-- @param what a string describing what is being tried
function _M:canBe(what)
	if what == "poison" and rng.percent(100 * (self:attr("poison_immune") or 0)) then return false end
	if what == "cut" and rng.percent(100 * (self:attr("cut_immune") or 0)) then return false end
	if what == "confusion" and rng.percent(100 * (self:attr("confusion_immune") or 0)) then return false end
	if what == "blind" and rng.percent(100 * (self:attr("blind_immune") or 0)) then return false end
	if what == "stun" and rng.percent(100 * (self:attr("stun_immune") or 0)) then return false end
	if what == "fear" and rng.percent(100 * (self:attr("fear_immune") or 0)) then return false end
	if what == "knockback" and rng.percent(100 * (self:attr("knockback_immune") or 0)) then return false end
	if what == "instakill" and rng.percent(100 * (self:attr("instakill_immune") or 0)) then return false end
	return true
end

function _M:getMaxEncumbrance()
	return math.floor(40 + self:getStr() * 1.8) + (self.max_encumber or 0)
end

function _M:getEncumbrance()
	-- Compute encumbrance
	local enc = 0
	for inven_id, inven in pairs(self.inven) do
		for item, o in ipairs(inven) do
			o:forAllStack(function(so) enc = enc + so.encumber end)
		end
	end
--	print("Total encumbrance", enc)
	return enc
end

function _M:checkEncumbrance()
	-- Compute encumbrance
	local enc, max = self:getEncumbrance(), self:getMaxEncumbrance()

	-- We are pinned to the ground if we carry too much
	if not self.encumbered and enc > max then
		game.logPlayer(self, "#FF0000#You carry too much, you are encumbered!")
		game.logPlayer(self, "#FF0000#Drop some of your items.")
		self.encumbered = self:addTemporaryValue("never_move", 1)
	elseif self.encumbered and enc <= max then
		self:removeTemporaryValue("never_move", self.encumbered)
		self.encumbered = nil
		game.logPlayer(self, "#00FF00#You are no longer encumbered.")
	end
end

--- Call when an object is added
function _M:onAddObject(o)
	engine.interface.ActorInventory.onAddObject(self, o)

	self:checkEncumbrance()
end

--- Call when an object is removed
function _M:onRemoveObject(o)
	engine.interface.ActorInventory.onRemoveObject(self, o)

	self:checkEncumbrance()
end

function _M:magicMap(radius, x, y)
	x = x or self.x
	y = y or self.y
	radius = math.floor(radius)
	for i = x - radius, x + radius do for j = y - radius, y + radius do
		if game.level.map:isBound(i, j) and core.fov.distance(x, y, i, j) < radius then
			game.level.map.remembers(i, j, true)
			game.level.map.has_seens(i, j, true)
		end
	end end
end

-- @param terrain Grid object that the actor is climbing
-- @return climbing speed, with 1 being full speed (minimum of 0.1)
function _M:canClimb(terrain)
	-- Check difficulty
	if not terrain.climbable then return false end
	if self:getTalentLevel(self.T_CLIMBING) < (terrain.climbable.difficulty or 0) then return false end
	return math.max(0.1, self.climbing_speed * (terrain.climbable.speed_mod or 1))
end

--- Returns the range of a talent
-- @return targets table/array containing the target types
function _M:getTalentTargets(t)
	if not t.targets then return {} end
	if type(t.targets) == "function" then return t.targets(self, t) end
	return t.targets
end

-- @return targets
function _M:resolveTalentTargets(t)
	targets = {}
	for i, typ in ipairs(self:getTalentTargets(t)) do
		x, y, target = self:getTarget(typ)
		table.insert(targets, {x=x, y=y, target=target, typ=typ})
	end
	return targets
end

-- @param t talent to validate the targets with
-- @param provided_targets table array of targets to validate
function _M:validateTalentTargets(t, provided_targets)
	local target_types = self:getTalentTargets(t)
	for i, target in ipairs(provided_targets) do
		if not self:canProject(target_types[i], target.x, target.y) then return false end
	end
	return true
end

function _M:projectile(t, x, y, damtype, dam, particles)
	if type(particles) ~= "function" and type(particles) ~= "table" then particles = nil end

	local typ = engine.Target:getType(t)
	-- Convert the destination to maximum distance along this angle
	local target_x, target_y
	local current_angle = math.atan2((y - self.y), (x - self.x)) + math.pi
	local angle_fudge = typ.angle_fudge or 0
	current_angle = current_angle + math.rad(rng.range(-angle_fudge, angle_fudge))
	target_x = self.x - math.floor(0.5 + (typ.range * math.cos(current_angle)))
	target_y = self.y - math.floor(0.5 + (typ.range * math.sin(current_angle)))

	local proj = require(self.projectile_class):makeProject(self, t.display, {x=x, y=y, start_x = t.x or self.x, start_y = t.y or self.y, damtype=damtype, tg=t, typ=typ, dam=dam, particles=particles})
	game.zone:addEntity(game.level, proj, "projectile", self.x, self.y)
end